/*
 * 文件名：ExcelParseUtil.java
 * 版权：Copyright by http://www.infopub.top/
 * 描述：
 * 修改人：Awoke
 * 修改时间：2018年11月9日
 * 跟踪单号：
 * 修改单号：
 * 修改内容：
 */

package top.infopub.mgr.util;


import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.NumberUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.multipart.MultipartFile;

import top.infopub.mgr.common.vo.ErrorCodeEnum;
import top.infopub.mgr.common.vo.RspVo;


/**
 * Excel 导入解析工具类
 * @author Awoke
 * @version 2018年11月9日
 * @see ExcelParseUtil
 * @since
 */
@Component
public final class ExcelParseUtil<T> {

    private final Logger logger = LoggerFactory.getLogger(ExcelParseUtil.class);

    /**
     * 支持的导入文件的格式
     */
    private static final String[] SUFFIX_PATTERN = {".xls", ".xlsx"};

    /**
     * 上传的excel流文件，解析获得其中数据，当文件单元格格式验证不通过时，会直接返回不规范行信息</br>
     * <B>当且仅当所有行都验证通过时，才返回完整的List数据</B>
     * @param file
     * @param clazz
     * @param rowStart 从第rowStart+1行开始读数据
     * @return 
     * @see 
     */
    @SuppressWarnings("unchecked")
    public RspVo<T> parser(MultipartFile file, Class<T> clazz, int rowStart) {
        if (null != SUFFIX_PATTERN && SUFFIX_PATTERN.length > 0) {
            FilenameFilter filter = new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    for (int i = 0; i < SUFFIX_PATTERN.length; i++ ) {
                        if (name.endsWith(SUFFIX_PATTERN[i].toLowerCase())) {
                            return true;
                        }
                    }
                    return false;
                }
            };
            if (!filter.accept(null, file.getOriginalFilename())) {
                return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo("文件格式不支持，请选择以下格式的文件："
                                                                 + Arrays.asList(SUFFIX_PATTERN));
            }
        }
        // 解析流
        try {
            // 区分 xlsx 和 xls
            String suffix = FilenameUtils.getExtension(file.getOriginalFilename());
            if ("xlsx".equalsIgnoreCase(suffix)) {
                return convertStreamXlsx(file.getInputStream(), clazz, rowStart);
            }
            else {
                return convertStreamXls(file.getInputStream(), clazz, rowStart);
            }
        }
        catch (Exception e) {
            logger.error("", e);
            return ErrorCodeEnum.ERROR_COMMON_SYSTEM.getRspVo("excel解析失败");
        }
    }

    /**
     * 解析excel流  (2007)
     * @throws IOException 
     */
    private RspVo<T> convertStreamXlsx(InputStream fis, Class<T> clazz, int rowStart)
        throws IOException {
        XSSFWorkbook hwk = new XSSFWorkbook(fis);
        // 得到book第一个工作薄sheet
        XSSFSheet sheet = hwk.getSheetAt(0);
        return convertBeanXlsx(sheet, clazz, rowStart);
    }

    /**
     * 解析excel流  (2003)
     * @throws IOException 
     */
    private RspVo<T> convertStreamXls(InputStream fis, Class<T> clazz, int rowStart)
        throws IOException {
        HSSFWorkbook hwk = new HSSFWorkbook(fis);
        // 得到book第一个工作薄sheet
        HSSFSheet sheet = hwk.getSheetAt(0);
        return convertBeanXls(sheet, clazz, rowStart);
    }

    /**
     * 解析excel-row成bean (2007)
     * @param sheet excel解析而来的第一个sheet
     * @param targetClazz 使用了ExcelParseBeanAnno注解目标要转化成的bean.class
     * @return 
     * @see
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private RspVo<T> convertBeanXlsx(XSSFSheet sheet, Class<T> targetClazz, int rowStart) {
        // 总行数（从0行开始）
        int rows = sheet.getLastRowNum();
        // 内容行小于rowStart行，数据不完整
        if (rows < rowStart) {
            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo("无可用的导入数据~");
        }
        // 目标clazz字段 
        Field[] fields_check = targetClazz.getDeclaredFields();
        for (Field field : fields_check) {
            if (!field.isAnnotationPresent(ExcelParseBeanAnno.class)) {
                return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo("解析bean中所有字段必须使用注解@ExcelParseBeanAnno");
            }
        }
        // 存放解析解析后的data
        List<T> dataList = new LinkedList<>();
        for (int r = rowStart; r <= rows; r++ ) {
            // row
            XSSFRow row = sheet.getRow(r);
            // 反射生成bean
            T result = newInstance(targetClazz);
            // 目标clazz字段 
            Field[] fields = targetClazz.getDeclaredFields();
            // 遍历赋值字段
            for (Field field : fields) {
                ExcelParseBeanAnno anno = field.getAnnotation(ExcelParseBeanAnno.class);
                // 列序号
                int colIdx = anno.colIdx();
                // 汉字列名
                String colCcName = anno.colCcName();
                // 解析row获得的值
                Object rowValue = null;
                // 获得字段类型
                Class fieldType = field.getType();
                // Number型
                if (Number.class.isAssignableFrom(fieldType)) {
                    try {
                        rowValue = row.getCell(colIdx).getNumericCellValue();
                        // 转成对应类型 这里用BigDecimal转下 防止double的科学计数法E直接转成String
                        rowValue = NumberUtils.parseNumber(
                            transferZero(new BigDecimal(String.valueOf(rowValue)).stripTrailingZeros().toPlainString()),
                            fieldType);
                    }
                    catch (Exception e) {
                        // 解析失败后再尝试用string解析一次，如果还失败则抛出验证不通过
                        try {
                            String strNumVal = row.getCell(colIdx).getStringCellValue();
                            rowValue = NumberUtils.parseNumber(strNumVal, fieldType);
                        }
                        catch (Exception e1) {
                            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo(warnMsg(r, colIdx,
                                colCcName, "数字格式"));
                        }
                    }
                }
                // Date型
                else if (Date.class.isAssignableFrom(fieldType)) {
                    try {
                        rowValue = row.getCell(colIdx).getDateCellValue();
                    }
                    catch (Exception e) {
                        // 解析失败后再尝试用string解析一次，如果还失败则抛出验证不通过
                        try {
                            String strDateVal = row.getCell(colIdx).getStringCellValue();
                            // 这里兼容各种日期格式
                            rowValue = DateUtils.parseDate(strDateVal, new String[] {
                                "yyyy-MM-dd HH:mm:ss", "yyyy年MM月dd日 HH时mm分ss秒", "yyyy-MM-dd",
                                "yyyyMMdd", "yyyy年MM月dd日"});
                        }
                        catch (Exception e1) {
                            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo(warnMsg(r, colIdx,
                                colCcName, "日期格式"));
                        }
                    }
                }
                // 其余皆 按照string解析
                else {
                    try {
                        if (row.getCell(colIdx) == null) {
                            rowValue = null;
                        }
                        else {
                            rowValue = row.getCell(colIdx).getStringCellValue();
                        }
                    }
                    catch (Exception e) {
                        // 解析失败后再尝试用number解析一次，如果还失败则抛出验证不通过
                        try {
                            double numStrVal = row.getCell(colIdx).getNumericCellValue();
                            // 这里用BigDecimal转下 防止double的科学计数法E直接转成String
                            rowValue = transferZero(new BigDecimal(String.valueOf(numStrVal)).stripTrailingZeros().toPlainString());
                        }
                        catch (Exception e1) {
                            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo(warnMsg(r, colIdx,
                                colCcName, "字符串"));
                        }
                    }
                }
                // 反射赋值字段
                doSetterMethod(result, field.getName(), field.getType(), rowValue);
            }
            // add into list 一行转成一个bean对象
            dataList.add(result);
        }
        // 最终返回解析结果
        return RspVo.getSuccessResponse(rows, dataList);
    }

    /**
     * 解析excel-row成bean (2003)
     * @param sheet excel解析而来的第一个sheet
     * @param targetClazz 使用了ExcelParseBeanAnno注解目标要转化成的bean.class
     * @return 
     * @see
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private RspVo<T> convertBeanXls(HSSFSheet sheet, Class<T> targetClazz, int rowStart) {
        // 总行数（从0行开始）
        int rows = sheet.getLastRowNum();
        // 内容行小于rowStart行，数据不完整
        if (rows < rowStart) {
            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo("无可用的导入数据~");
        }
        // 目标clazz字段 
        Field[] fields_check = targetClazz.getDeclaredFields();
        for (Field field : fields_check) {
            if (!field.isAnnotationPresent(ExcelParseBeanAnno.class)) {
                return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo("解析bean中所有字段必须使用注解@ExcelParseBeanAnno");
            }
        }
        // 存放解析解析后的data
        List<T> dataList = new LinkedList<>();
        for (int r = rowStart; r <= rows; r++ ) {
            // row
            HSSFRow row = sheet.getRow(r);
            // 反射生成bean
            T result = newInstance(targetClazz);
            // 目标clazz字段 
            Field[] fields = targetClazz.getDeclaredFields();
            // 遍历赋值字段
            for (Field field : fields) {
                ExcelParseBeanAnno anno = field.getAnnotation(ExcelParseBeanAnno.class);
                // 列序号
                int colIdx = anno.colIdx();
                // 汉字列名
                String colCcName = anno.colCcName();
                // 解析row获得的值
                Object rowValue = null;
                // 获得字段类型
                Class fieldType = field.getType();
                // Number型
                if (Number.class.isAssignableFrom(fieldType)) {
                    try {
                        rowValue = row.getCell(colIdx).getNumericCellValue();
                        // 转成对应类型 这里用BigDecimal转下 防止double的科学计数法E直接转成String
                        rowValue = NumberUtils.parseNumber(
                            transferZero(new BigDecimal(String.valueOf(rowValue)).stripTrailingZeros().toPlainString()),
                            fieldType);
                    }
                    catch (Exception e) {
                        // 解析失败后再尝试用string解析一次，如果还失败则抛出验证不通过
                        try {
                            String strNumVal = row.getCell(colIdx).getStringCellValue();
                            rowValue = NumberUtils.parseNumber(strNumVal, fieldType);
                        }
                        catch (Exception e1) {
                            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo(warnMsg(r, colIdx,
                                colCcName, "数字格式"));
                        }
                    }
                }
                // Date型
                else if (Date.class.isAssignableFrom(fieldType)) {
                    try {
                        rowValue = row.getCell(colIdx).getDateCellValue();
                    }
                    catch (Exception e) {
                        // 解析失败后再尝试用string解析一次，如果还失败则抛出验证不通过
                        try {
                            String strDateVal = row.getCell(colIdx).getStringCellValue();
                            // 这里兼容各种日期格式
                            rowValue = DateUtils.parseDate(strDateVal, new String[] {
                                "yyyy-MM-dd HH:mm:ss", "yyyy年MM月dd日 HH时mm分ss秒", "yyyy-MM-dd",
                                "yyyyMMdd", "yyyy年MM月dd日"});
                        }
                        catch (Exception e1) {
                            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo(warnMsg(r, colIdx,
                                colCcName, "日期格式"));
                        }
                    }
                }
                // 其余皆 按照string解析
                else {
                    try {
                        if (row.getCell(colIdx) == null) {
                            rowValue = null;
                        }
                        else {
                            rowValue = row.getCell(colIdx).getStringCellValue();
                        }
                    }
                    catch (Exception e) {
                        // 解析失败后再尝试用number解析一次，如果还失败则抛出验证不通过
                        try {
                            double numStrVal = row.getCell(colIdx).getNumericCellValue();
                            // 这里用BigDecimal转下 防止double的科学计数法E直接转成String
                            rowValue = transferZero(new BigDecimal(String.valueOf(numStrVal)).stripTrailingZeros().toPlainString());
                        }
                        catch (Exception e1) {
                            return ErrorCodeEnum.ERROR_COMMON_CHECK.getRspVo(warnMsg(r, colIdx,
                                colCcName, "字符串"));
                        }
                    }
                }
                // 反射赋值字段
                doSetterMethod(result, field.getName(), field.getType(), rowValue);
            }
            // add into list 一行转成一个bean对象
            dataList.add(result);
        }
        // 最终返回解析结果
        return RspVo.getSuccessResponse(rows, dataList);
    }

    /**
     * newInstance
     * @param clazz
     * @return 
     * @see
     */
    private T newInstance(Class<T> clazz) {
        try {
            return clazz.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException("new instance exception", e);
        }
    }

    /**
     * set字段属性
     * @param result
     * @param field
     * @param type
     * @param params 
     * @see
     */
    private void doSetterMethod(Object result, String field, Class<?> type, Object... params) {
        try {
            String setterMethodName = "set" + field.substring(0, 1).toUpperCase()
                                      + field.substring(1);
            Method setterMethod = ReflectionUtils.findMethod(result.getClass(), setterMethodName,
                type);
            setterMethod.invoke(result, params);
        }
        catch (Exception e) {
            throw new RuntimeException("do setter exception ", e);
        }
    }

    /**
     * warnMsg
     */
    private String warnMsg(int rowNum, int colIdx, String colCcName, String type) {
        StringBuilder warn = new StringBuilder();
        warn.append("第").append((rowNum + 1)).append("行第").append((colIdx + 1)).append("列：").append(
            colCcName).append(" 应为").append(type);
        return warn.toString();
    }

    /** 将0.0转成0 其他直接返回  兼容BigDecimal stripTrailingZeros后仍然返回0.0 **/
    private String transferZero(String value) {
        if ("0.0".equals(value)) {
            return "0";
        }
        else {
            return value;
        }
    }

}
